"""Test cases for the slots app, Slot model.

    * creation
    * disallowing clashes
    * shrinking of existing slots
    * error handling
    * deletion of models causing slots to disappear
"""

import os
import datetime
from time import sleep

from django.test import TestCase
from django.conf import settings

import slots.models as slots_models
from slots.tests.helpers import create_models, destroy_models, \
                                           Header, Page, Sponsor, TimingTest


# XXX tests start here
class SlotTests(TimingTest):
    def create_slot(self, header, start, end):
        "Relates the given header to self.page between ``start`` and ``end``"
        return self.page.add_to_schedule(header, start, end)
    def create_five_second_slot_in_five_seconds_time(self,header):
        now = datetime.datetime.now()
        start_time = now + self.five_seconds
        end_time = start_time + self.five_seconds
        slot = self.create_slot(header,start_time,end_time)
        return slot
    def testPageHasMixinAttributes(self):
        """Does ``Page`` have the attributes contributed by the mixin?"""
        # these two are the explicit ones, if they fail then python's broken
        self.assert_(hasattr(self.page, 'schedules'))
        self.assertEqual(self.page.schedules, (Header,))
        # now check the extra ones we should get
        # properties
        self.assert_(hasattr(self.page, 'schedule'))
        self.assert_(hasattr(self.page, 'current'))
        self.assert_(hasattr(self.page, 'next'))
        self.assert_(hasattr(self.page, 'last'))
        # methods
        self.assert_(hasattr(self.page, 'schedule_for_model'))
        self.assert_(hasattr(self.page, 'current_for_model'))
        self.assert_(hasattr(self.page, 'next_for_model'))
        self.assert_(hasattr(self.page, 'last_for_model'))
        self.assert_(hasattr(self.page, 'add_to_schedule'))
    def testPageHasNothingScheduledAtAll(self):
        """Does a normal ``Page`` have nothing scheduled against it?"""
        # entire schedule - should only have one key, 'Header', whose value
        # is an empty queryset (of Slots)
        schedule = self.page.schedule
        self.assertEqual(len(schedule.keys()),1)
        self.assert_(schedule.has_key('Header'))
        self.assertEqual(list(schedule['Header']),[])
        # schedule for header - have to cast it to a list (it's an empty
        # queryset)
        self.assertEqual(list(self.page.schedule_for_model('Header')),[])
        # instead of querysets, current returns objects or None as the values
        # not slot objects!
        current = self.page.current
        self.assertEqual(len(current.keys()),1)
        self.assert_(current.has_key('Header'))
        self.assertEqual(current['Header'],None)
        # current header
        self.assertEqual(self.page.current_for_model('Header'),None)
        # everything next
        self.assertEqual(self.page.next(),{'Header':None})
        # next header
        self.assertEqual(self.page.next_for_model('Header'),None)
        # everything last (previous, not last one ever)
        self.assertEqual(self.page.last(),{'Header':None})
        # last header
        self.assertEqual(self.page.last_for_model('Header'),None)
    def testHasPageCreatedGenericIntermediary(self):
        """Has initialising self.page created the GenericIntermediary object?"""
        self.assert_(self.page._meta.gi.has_key('Header'))
        # the left and right hand side of this relationship should be the
        # content types of our two models
        self.assertEqual(self.page._meta.gi['Header'].left, self.page_ct)
        self.assertEqual(self.page._meta.gi['Header'].right, self.header_ct)
    def testSlotCreation(self):
        """Does scheduling self.header1 create a Slot?"""
        self.assertEqual(slots_models.Slot.objects.all().count(),0)
        self.create_five_second_slot_in_five_seconds_time(self.header1)
        self.assertEqual(slots_models.Slot.objects.all().count(),1)
    def testSlotCreationPopulatesSchedule(self):
        """Does scheduling self.header1 make .schedule/.next/etc work?"""
        # use the above test to create our slot
        # the slot it creates lasts for 5 seconds, and starts in 5 seconds
        self.testSlotCreation()
        # schedule returns a dictionary of querysets of Slots
        # cast it to a list to make comparison work at all, and make sure we get
        # a Slot back (the only one there is, as confirmed by testSlotCreation)
        slot = slots_models.Slot.objects.all()[0]
        self.assertEqual(list(self.page.schedule['Header']),
                         [slot])
        # now going to test current
        # now test that the Slot represents self.header1 referring to self.page1
        # these attributes are the objects on each side of the relationship
        self.assertEqual(slot.against, self.page)
        self.assertEqual(slot.slotted, self.header1)
        # now check the other methods - first the schedule
        self.assertEqual(list(self.page.schedule_for_model('Header')),[slot])
        # now check next.  hope this happens < 5s before the test started!
        # 
        # next returns a dictionary: keyed on the object
        # the values are themselves dictionaries: keys of 'starts' and
        # 'object'
        # 'starts' is a timestamp, 'object' is the actual object
        next = self.page.next()
        self.assert_(next.has_key('Header'))
        self.assertEqual(next['Header']['object'],self.header1)
        # next_for misses out the master dictionary, since you've asked for it
        # by name -- but you still get the starts+object stuff
        self.assertEqual(self.page.next_for_model('Header')['object'],self.header1)
        # now sleep for 7s, thinking this should get us into the middle of the
        # slot(!)
        sleep(7)
        current = self.page.current
        self.assertEqual(current['Header'], self.header1)
        # current_for gives us just the object
        self.assertEqual(self.page.current_for_model('Header'), self.header1)
        # now check last.  but this won't work until we've slept
        sleep(4)
        # last and last_for operate in the same way as next and next_for
        last = self.page.last()
        self.assert_(last.has_key('Header'))
        self.assertEqual(last['Header']['object'],self.header1)
        # last_for misses out the master dictionary, since you've asked for it
        # by name -- but you still get the starts+object stuff
        self.assertEqual(self.page.last_for_model('Header')['object'],self.header1)
    def testSlotCannotBeInsideAnotherSlot(self):
        "Can a slot fall entirely within an existing slot?"
        slot=self.create_five_second_slot_in_five_seconds_time(self.header2)
        new_start=slot.start_time + self.one_second
        new_end=slot.end_time - self.one_second
        self.assertRaises(slots_models.SlotError,
                          lambda: self.create_slot(self.header2,
                                                   new_start,
                                                   new_end))
        # make sure it throws the _right_ SlotError
        try:
            self.create_slot(self.header2, new_start, new_end)
        except slots_models.SlotError, s:
            self.assertEqual(s.type,'clash')
            self.assert_(str(s).startswith('error with slot'))
            self.assertEqual(s.message,'slot falls entirely within another')
    def testSlotCannotSurroundAnotherSlot(self):
        "Can a slot entirely surround an existing slot?"
        slot=self.create_five_second_slot_in_five_seconds_time(self.header2)
        new_start=slot.start_time - self.one_second
        new_end=slot.end_time + self.one_second
        self.assertRaises(slots_models.SlotError,
                          lambda: self.create_slot(self.header2,
                                                   new_start, new_end))
        # make sure it throws the _right_ SlotError
        try:
            self.create_slot(self.header2, new_start, new_end)
        except slots_models.SlotError, s:
            self.assertEqual(s.type,'clash')
            self.assert_(str(s).startswith('error with slot'))
            self.assertEqual(s.message,'slot overwrites one or more existing slots')
    def testSlotStartAndEndCannotMatch(self):
        "Can a slot's start and end time match?"
        start = datetime.datetime.now()
        end = start
        self.assertRaises(slots_models.SlotError,
                          lambda: self.create_slot(self.header2,
                                                   start, end))
        # make sure it throws the _right_ SlotError
        try:
            self.create_slot(self.header2, start, end)
        except slots_models.SlotError, s:
            self.assertEqual(s.type,'timing')
            self.assert_(str(s).startswith('start and end time are the same'))
    def testSlotStartCannotBeBeforeEnd(self):
        "Can a slot's start time be before the end time?"
        start = datetime.datetime.now()
        end = start - self.five_seconds
        self.assertRaises(slots_models.SlotError,
                          lambda: self.create_slot(self.header2, start, end))
        # make sure it throws the _right_ SlotError
        try:
            self.create_slot(self.header2, start, end)
        except slots_models.SlotError, s:
            self.assertEqual(s.type,'timing')
            self.assert_(str(s).find('later than end time') > 0)
    def testOverlappingSlotStartShrinksNextSlot(self):
        "Does a new slot shrink an existing one, if it overlaps at the start?"
        slot=self.create_five_second_slot_in_five_seconds_time(self.header2)
        # record the start time of the created slot
        first_start = slot.start_time
        # now use it to create a new one which starts five seconds
        # before, and ends a second after
        new_start = (first_start - self.five_seconds).replace(microsecond=0)
        new_end = (first_start + self.one_second).replace(microsecond=0)
        new_slot = self.create_slot(self.header1, new_start, new_end)
        # re-get the original slot and see if its start has moved
        # to a second after our new slot's end time
        original_slot = slots_models.Slot.objects.get(pk=slot.id)
        expected = (new_end + self.one_second).replace(microsecond=0)
        self.assertEquals(expected,original_slot.start_time)
        
    def testOverlappingSlotEndShrinksPreviousSlot(self):
        "Does a new slot shrink an existing one, if it overlaps at the end?"
        slot=self.create_five_second_slot_in_five_seconds_time(self.header2)
        # record the end time of the created slot
        first_end = slot.end_time
        # now use it to create a new one which starts one second
        # before, and ends five seconds after
        new_start = (first_end - self.one_second).replace(microsecond=0)
        new_end = (first_end + self.five_seconds).replace(microsecond=0)
        new_slot = self.create_slot(self.header1, new_start, new_end)
        # re-get the original slot and see if its end has moved
        # to a second before our new slot's start time
        original_slot = slots_models.Slot.objects.get(pk=slot.id)
        expected = (new_start - self.one_second).replace(microsecond=0)
        self.assertEquals(expected,original_slot.end_time)

    def testDoubleOverlappingSlotShrinksSurroundingSlots(self):
        "Does a new slot shrink both existing ones, if it overlaps them?"
        slot_one=self.create_five_second_slot_in_five_seconds_time(self.header2)
        first_end = slot_one.end_time
        # now create another slot, starting one second after the last
        # one finished
        slot_two = self.create_slot(self.header1, first_end + self.one_second,
                                    first_end + self.five_seconds)
        second_start = slot_two.start_time
        # now create a third one: two seconds before the first one's
        # end, and two seconds after the second one's start
        slot_three = self.create_slot(self.header1, first_end - self.two_seconds,
                                      second_start + self.two_seconds)
        # the first one should now end 3 seconds before it did
        expected_first_end = (first_end - self.three_seconds).replace(microsecond=0)
        # and the second one should now start 3 seconds after it did
        expected_second_start = (second_start + self.three_seconds).replace(microsecond=0)
        # now re-get the slots and see if they've been modified correctly
        candidate_slot = slots_models.Slot.objects.get(pk=slot_one.id)
        self.assertEquals(expected_first_end,candidate_slot.end_time)
        candidate_slot = slots_models.Slot.objects.get(pk=slot_two.id)
        self.assertEquals(expected_second_start,candidate_slot.start_time)
    def testDeletingScheduledObjectDeletesSlot(self):
        "Does deleting a scheduled header remove the slot?"
        # create a slots and then delete the object on the right --
        # the slot should disappear
        self.create_five_second_slot_in_five_seconds_time(self.header2)
        self.assertEqual(slots_models.Slot.objects.all().count(),1)
        self.header2.delete()
        self.assertEqual(slots_models.Slot.objects.all().count(),0)
    def testDeletingTargetObjectDeletesSlot(self):
        "Does deleting the page remove the slots?"
        # create a slots and then delete the object on the left --
        # the slot should disappear
        self.create_five_second_slot_in_five_seconds_time(self.header2)
        self.assertEqual(slots_models.Slot.objects.all().count(),1)
        self.page.delete()
        self.assertEqual(slots_models.Slot.objects.all().count(),0)
    def testSchedulingSomethingNotInDeclarationFails(self):
        "Does trying to schedule a model not in the schedules attribute fail?"
        self.assertRaises(ValueError,
                          lambda:
                          self.create_five_second_slot_in_five_seconds_time(self.sponsor))
    def testQueryingSomethingNotInDeclarationFails(self):
        "Does querying a schedule for an invalid model fail?"
        self.assertRaises(ValueError, lambda: self.page.schedule_for_model('Sponsor'))
        self.assertRaises(ValueError, lambda: self.page.current_for_model('Sponsor'))
        self.assertRaises(ValueError, lambda: self.page.next_for_model('Sponsor'))
        self.assertRaises(ValueError, lambda: self.page.last_for_model('Sponsor'))

